package implementation1.app.streams;

import exceptions.StreamEOFException;
import exceptions.StreamException;
import exceptions.StreamReadingException;
import exceptions.StreamWritingException;
import interfaces.app.IStream;

import java.io.*;
import java.util.ResourceBundle;
import java.util.concurrent.Semaphore;

/**
 * Class for creating stream which works with text files.
 * Created: 22.10.11
 *
 * @author Jan Svab
 */
public class FileStream implements IStream
{
    /**
     * Resource of messages.
     */
    private ResourceBundle bundle = ResourceBundle.getBundle("implementation1.res.messages");

    /**
     * TRUE - stream is exclusive, FALSE - is not
     */
    private boolean isExclusive;
    /**
     * File for reading or writing
     */
    private File file;
    /**
     * TRUE - stream is open, FALSE - is not
     */
    private boolean isOpen;
    /**
     * Reference to a file reader
     */
    private BufferedReader fileReader;
    /**
     * Reference to a file writer
     */
    private BufferedWriter fileWriter;
    /**
     * Access attribute to stream
     */
    private int accessAttribute;

    private Semaphore mutex;

    private boolean notifyingProcesses = false;

    /**
     * Default constructor.
     *
     * @param filename a file for stream
     */
    public FileStream(String filename)
    {
        this.isExclusive = true;
        this.file = new File(filename);

        isOpen = false;

        fileWriter = null;
        fileReader = null;
        accessAttribute = -1;
        mutex = new Semaphore(1);
    }


    /**
     * Return true if the stream is opened.
     *
     * @return true if the stream is opened; otherwise false
     */
    @Override
    public boolean isOpen()
    {
        return isOpen;
    }

    /**
     * Return true if the stream is opened exclusively.
     *
     * @return true if the stream is opened exclusively; otherwise false
     */
    @Override
    public boolean isExclusive()
    {
        return isExclusive;
    }

    /**
     * Open the stream, if no possible return false.
     *
     * @param accessAttribute access to the stream; 1=read; 2=write
     * @return true if opening was successful; otherwise false
     */
    @Override
    public synchronized boolean tryOpen(int accessAttribute)
    {
        if (mutex.availablePermits() == 0)
        {
            return false;
        }

        try
        {
            open(accessAttribute);
        } catch (StreamException e)
        {
            return false;
        }

        return true;
    }

    /**
     * Open the stream, if no possible then wait.
     *
     * @param accessAttribute access to the stream; 1=read; 2=write
     * @throws StreamException Thrown if occurs error during opening of the stream.
     */
    @Override
    synchronized public void open(int accessAttribute) throws StreamException
    {
        if (isExclusive)
        {
            try
            {
                mutex.acquire();

                if (notifyingProcesses)
                {
                    throw new StreamException(bundle.getString("error.stream.openStream"));
                }
            } catch (InterruptedException e)
            {
                throw new StreamException(bundle.getString("error.stream.openStream"));
            }
        }

        if (accessAttribute == AccessAttributes.READ)
        {
            try
            {
                fileReader = new BufferedReader(new FileReader(file));
            } catch (FileNotFoundException e)
            {
                throw new StreamException(bundle.getString("error.stream.openForRead") + " '" + file.getAbsolutePath());
            }
        }
        else if (accessAttribute == AccessAttributes.WRITE)
        {
            try
            {
                fileWriter = new BufferedWriter(new FileWriter(file));
            } catch (IOException e)
            {
                throw new StreamException(bundle.getString("error.stream.openForWrite") + " '" + file.getAbsolutePath());
            }
        }
        else
        {
            throw new StreamException(bundle.getString("error.stream.wrongAccessAttribute"));
        }

        this.accessAttribute = accessAttribute;
        isOpen = true;
    }

    /**
     * Close the stream.
     *
     * @throws StreamException if stream can not be closed
     */
    @Override
    public synchronized void close() throws StreamException
    {
        if (fileReader != null)
        {
            try
            {
                fileReader.close();
            } catch (IOException e)
            {
                throw new StreamException(bundle.getString("error.stream.close.openForRead") + " '" + file.getAbsolutePath());
            }
        }

        if (fileWriter != null)
        {
            try
            {
                fileWriter.close();
            } catch (IOException e)
            {
                throw new StreamException(bundle.getString("error.stream.closeForWrite") + " '" + file.getAbsolutePath());
            }
        }

        if (isExclusive && isOpen)
        {
            mutex.release();
        }

        fileReader = null;
        fileWriter = null;
        isOpen = false;
        accessAttribute = -1;
    }

    /**
     * Read a line from the stream if can not read wait.
     *
     * @return the line from stream
     * @throws StreamException    Thrown if occurs error during reading.
     * @throws StreamEOFException Thrown if occurs EOF on input.
     */
    @Override
    synchronized public String readLine() throws StreamException, StreamEOFException
    {
        if (fileReader == null || !isOpen() || accessAttribute != AccessAttributes.READ)
        {
            throw new StreamReadingException(bundle.getString("error.stream.read") + " " + bundle.getString("error.stream.noForRead"));
        }

        try
        {
            if (!fileReader.ready())
            {
                throw new StreamEOFException();
            }
            return fileReader.readLine();
        } catch (IOException e)
        {
            throw new StreamReadingException(bundle.getString("error.stream.read"));
        }
    }

    /**
     * Write a line to the stream.
     *
     * @param line a line
     * @throws StreamException Thrown if occurs error during writing into stream.
     */
    @Override
    synchronized public void writeLine(String line) throws StreamException
    {
        if (fileWriter == null || !isOpen || accessAttribute != AccessAttributes.WRITE)
        {
            throw new StreamWritingException(bundle.getString("error.stream.write") + " " + bundle.getString("error.stream.noForWrite"));
        }

        try
        {
            fileWriter.write(line + "\r\n");
        } catch (IOException e)
        {
            throw new StreamWritingException(bundle.getString("error.stream.write"));
        }
    }

    /**
     * Notify all waiting process.
     */
    @Override
    public void notifyAllProcess()
    {
        synchronized (this)
        {
            this.notifyAll();
        }

        notifyingProcesses = true;
        while (mutex.hasQueuedThreads())
        {
            mutex.release();
        }
        notifyingProcesses = false;
    }

    @Override
    public void setExclusive(boolean exclusive)
    {
        throw new RuntimeException(bundle.getString("error.stream.fileStream.exclusivity"));
    }
}
